﻿using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics.CodeAnalysis;
using System.Dynamic;
using System.Reflection;
using System.Threading.Tasks;

namespace RingBufferStream.Utils.Database
{
  /// <summary>
  /// Base class to derive from for adding custom stored procedure calls.
  /// </summary>
  public class BaseStoredProcedure : IStoredProcedure, IAwaitable<int>, IAwaiter<int>
  {
    private string _commandText;
    private IDbDataParameter _returnValue;
    private Queue<Action> _continuations;
    private Exception _exception;

    /// <summary>
    /// Constructor for initializing the stored procedure name.
    /// </summary>
    /// <param name="procedureName">The stored procedure name.</param>
    protected BaseStoredProcedure(string procedureName)
    {
      _commandText = procedureName;
    }

    /// <summary>
    /// Integer return value containing the stored procedure return value.
    /// Only valid when return param was added with <see cref="AddReturnParameter"/>.
    /// </summary>
    public int ReturnValue { get; private set; }

    /// <summary>
    /// Invoke the stored procedure named with the <see cref="CommandText"/> property.
    /// </summary>
    /// <param name="connectionString">The connection string to use for the database connection.</param>
    /// <returns>Return value from the stored procedure (if added).</returns>
    public async Task<int> ExecuteAsync(string connectionString)
    {
      new SqlServerInvoke {ConnectionString = connectionString}.ExecuteAsync(this);
      return await this;
    }

    /// <summary>
    /// Method for adding a return parameter to the parameter collection of the command object.
    /// </summary>
    /// <param name="command">The database command to which to add the return parameter.</param>
    protected void AddReturnParameter(IDbCommand command)
    {
      if (command == null)
      {
        throw new ArgumentNullException("command");
      }
      IDbDataParameter parameter = command.CreateParameter();
      parameter.DbType = DbType.Int32;
      parameter.Direction = ParameterDirection.ReturnValue;
      int position = command.Parameters.Add(parameter);
      _returnValue = (IDbDataParameter)command.Parameters[position];
    }

    #region IStoredProcedure

    /// <summary>
    /// The stored procedure name to use during invocation.
    /// Must be set by the concrete class before executing the stored procedure.
    /// </summary>
    public virtual string CommandText
    {
      get { return _commandText; }
      set { _commandText = value; }
    }

    bool IStoredProcedure.HasResultSet
    {
      get { return HasResultSet; }
    }

    /// <summary>
    /// Override in concrete implementation to explicitly state whether a result set is expected.
    /// This serves as an optimization to reduce overhead when invoking the stored procedure.
    /// Default is true when the concrete implementation overrides the <see cref="ReadRecord(object)"/> method.
    /// </summary>
    protected virtual bool HasResultSet
    {
      get
      {
        MethodInfo methodInfo = GetType().GetMethod("ReadRecord", BindingFlags.Instance | BindingFlags.NonPublic);
        return methodInfo != null && methodInfo.DeclaringType == GetType();
      }
    }

    /// <summary>
    /// Virtual method to inherit if the concrete implementation wants to add and set parameters.
    /// </summary>
    /// <param name="command">The command object to add\set parameters on.</param>
    public virtual void SetParameters(IDbCommand command)
    {
    }

    void IStoredProcedure.StartRead(IDataReader reader)
    {
      StartRead(reader);
    }

    /// <summary>
    /// Virtual method to allow the concrete implementation to prepare any state on the reader before the execution.
    /// </summary>
    /// <param name="reader">The reader that will be used for the stored procedure invocation.</param>
    protected virtual void StartRead(IDataReader reader)
    {
    }

    void IStoredProcedure.ReadRecord(IDataRecord record)
    {
      dynamic expando = new ExpandoObject();
      IDictionary<string, object> lookup = expando;
      for (int i = 0; i < record.FieldCount; ++i)
      {
        object value = record[i];
        lookup.Add(record.GetName(i), DBNull.Value.Equals(value) ? null : value);
      }
      ReadRecord(expando);
    }

    /// <summary>
    /// Virtual method to allow the concrete implementation to read the current field values from the data record.
    /// </summary>
    /// <param name="record">The reader instance with the data record.</param>
    protected virtual void ReadRecord(dynamic record)
    {
    }

    [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
    void IStoredProcedure.FinishRead(IDbCommand command)
    {
      if (_returnValue != null)
      {
        ReturnValue = (int)_returnValue.Value;
      }

      try
      {
        if (command != null && command.Parameters.Count > 0)
        {
          var expando = new ExpandoObject();
          IDictionary<string, object> lookup = expando;
          foreach (IDataParameter parameter in command.Parameters)
          {
            if (parameter.Direction == ParameterDirection.Input || parameter.Direction == ParameterDirection.ReturnValue)
            {
              continue;
            }
            object value = parameter.Value;
            lookup.Add(parameter.ParameterName.Remove(0, 1), DBNull.Value.Equals(value) ? parameter.DefaultValue() : value);
          }
          if (lookup.Count > 0)
          {
            ReadOutputParameters(expando);
          }
        }
        FinishRead(command);
      }
      catch (Exception exception)
      {
        _exception = exception;
      }
      RestoreAwaitContext();
    }

    void IStoredProcedure.FinishRead(Exception exception)
    {
      _exception = exception;
      RestoreAwaitContext();
    }

    private void RestoreAwaitContext()
    {
      lock (_mutex)
      {
        if (_isCompleted)
        {
          return;
        }
        _isCompleted = true;
        InvokeContinuations();
      }
    }

    private void InvokeContinuations()
    {
      if (_continuations == null)
      {
        return;
      }
      do
      {
        _continuations.Dequeue()();
      } while (_continuations.Count > 0);
    }

    /// <summary>
    /// Virtual method to allow the concrete implementation to read the values for any output parameters added with the <see cref="SetParameters"/> method.
    /// </summary>
    /// <param name="parameters">The command instance that was used for connecting and executing the stored procedure.</param>
    protected virtual void ReadOutputParameters(dynamic parameters)
    {
    }

    protected virtual void FinishRead(IDbCommand command)
    {
      if (ReturnValue != 0)
      {
        _exception =  new SqlReturnValueException("Database error occurred.", ReturnValue);
      }
    }

    #endregion

    #region IAwaiter

    private readonly object _mutex = new object();
    private bool _isCompleted;

    public bool IsCompleted
    {
      get
      {
        lock (_mutex)
        {
          return _isCompleted;
        }
      }
    }

    public int GetResult()
    {
      if (_exception != null)
      {
        throw _exception;
      }
      return ReturnValue;
    }
    #endregion

    #region IAwaitable
    public IAwaiter<int> GetAwaiter()
    {
      return this;
    }

    public void OnCompleted(Action continuation)
    {
      if (continuation == null)
      {
        throw new ArgumentNullException("continuation");
      }

      lock (_mutex)
      {
        if (_isCompleted)
        {
          continuation();
          return;
        }
        if (_continuations == null)
        {
          _continuations = new Queue<Action>(4);
        }
        _continuations.Enqueue(continuation);
      }
    }
    #endregion
  }
}
